package cn.demoncat.util.io;

import cn.demoncat.util.exception.IoRuntimeException;
import cn.demoncat.util.exception.ParamRuntimeException;
import cn.demoncat.util.io.entity.InsFile;
import cn.demoncat.util.lang.constant.CharsetConstant;
import cn.demoncat.util.lang.constant.StringConstant;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Zip压缩工具
 * 
 * @author 延晓磊
 *
 * @since 2016年1月21日
 */
public final class ZipUtil {

	private static final String ZIP_TAG = ".zip";
	
	// 异常消息
	private static final String EXP_ZIP_OUT = "压缩并输出Zip文件失败：";
	
	/**
	 * 读取ZIP文件
	 * 
	 * @param zip	ZIP输入流
	 * @return {name:data}
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年4月2日
	 */
	public static Map<String, byte[]> read(InputStream zip){
		return read(zip, null);
	}

	/**
	 * 读取ZIP文件
	 * 
	 * @param zip		ZIP输入流
	 * @param pattern	文件名格式
	 * @return {name:data}
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年4月2日
	 */
	public static Map<String, byte[]> read(InputStream zip, String pattern){
		Map<String, byte[]> map = new HashMap<>();
		ZipArchiveInputStream zin = null;
		ArchiveEntry entry;
        String name;
        try {
        	// 创建ZIP
        	zin = new ZipArchiveInputStream(zip);
        	// 遍历ZIP条目
			byte[] bytes;
			while ((entry = zin.getNextEntry()) != null) {
    			// 文件
    			if (!entry.isDirectory()) {
    				// 匹配文件名
    				name = entry.getName();
    				if (pattern == null || name.matches(pattern)) {
    					if (entry.getSize() == 0) {
    						map.put(entry.getName(), new byte[0]);
    					}else {
    						// 读取条目
    						bytes = new byte[(int) entry.getSize()];
    						zin.read(bytes);
    						map.put(entry.getName(), bytes);
    					}
					}
    			}
    		}
        	return map;
        }catch (Exception e) {
			throw new IoRuntimeException("读取ZIP失败", e);
		} finally {
			CloseUtil.close(zin);
			CloseUtil.close(zip);
		}
	}
	
	/**
	 * 读取ZIP文件
	 * 
	 * @param zip		ZIP输入流
	 * @param encode	编码
	 * @return {name:data}
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年4月2日
	 */
	public static Map<String, String> readToStr(InputStream zip, String encode){
		return readToStr(zip, encode, null);
	}
	
	/**
	 * 读取ZIP文件
	 * 
	 * @param zip		ZIP输入流
	 * @param encode	编码
	 * @param pattern	文件名格式
	 * @return {name:data}
	 * 
	 * @author 延晓磊
	 *
	 * @since 2020年4月2日
	 */
	public static Map<String, String> readToStr(InputStream zip, String encode, String pattern){
		// 编码
		Charset cs;
		if (StringUtils.isBlank(encode)) {
			cs = CharsetConstant.UTF8_CHARSET;
		}else {
			cs = Charset.forName(encode);
		}
		// 读取
		Map<String, String> map = new HashMap<>();
		ZipArchiveInputStream zin = null;
		byte[] bytes;
        ArchiveEntry entry;
        String name;
        try {
        	// 创建ZIP
        	zin = new ZipArchiveInputStream(zip);
        	// 遍历ZIP条目
        	while ((entry = zin.getNextEntry()) != null) {
    			// 文件
    			if (!entry.isDirectory()) {
    				// 匹配文件名
    				name = entry.getName();
    				if (pattern == null || name.matches(pattern)) {
    					if (entry.getSize() == 0) {
    						map.put(name, StringConstant.EMPTY);
    					}else {
    						// 读取条目
    						bytes = new byte[(int) entry.getSize()];
    						zin.read(bytes);
    						map.put(name, StringUtils.toEncodedString(bytes, cs));
    					}
					}
    			}
    		}
        	return map;
        }catch (Exception e) {
			throw new IoRuntimeException("读取ZIP失败", e);
		} finally {
			CloseUtil.close(zin);
			CloseUtil.close(zip);
		}
	}
	
	
	
	/**
	 * 解压Zip文件
	 * 
	 * @param src
	 * @param descPath  
	 * @param keepZipName 是否保留Zip文件的名称(true：解压在descPath目录下；false：替换descPath目录，忽略压缩文件的名称)
	 * @throws IoRuntimeException 如果参数为空或无效，抛出
	 */
	public static void unzip(InputStream src, String descPath, boolean keepZipName){
		//参数校验
		if (src == null || StringUtils.isBlank(descPath)) {
			throw ParamRuntimeException.NULL;
		}
		//目标目录
		File desc = new File(descPath);
		//如果不保留Zip的文件名，则删除同名文件
		if (! keepZipName) {
			//强制删除同名的目标文件，防止输出时因文件类型问题而替换失败
			FileUtil.delFile(desc);
		}
		//强制创建目标目录，包含必要的父目录
		FileUtil.addDir(desc);
		//获取目标目录的完整Linux标准路径名，后缀/号
		descPath = FileNameUtil.fileNameFormat(desc.getAbsolutePath()+StringConstant.SEPARATOR, 1);
		//局部变量
		ZipArchiveInputStream zIn = null;
		ZipArchiveEntry entry;
		File entryFile;
		String entryFileName;
		boolean isDir;
		try {
			//根据InputStream创建Zip输入流，以UTF-8编码
			zIn = new ZipArchiveInputStream(src,CharsetConstant.UTF8);
			//循环获取Zip压缩文件的条目(没有下一个条目时返回null)
			while ((entry = zIn.getNextZipEntry()) != null) {
				//获取条目的名称
				entryFileName = entry.getName();
				//条目是否为目录
				isDir = entry.isDirectory();
				//如果不保留Zip的文件名，则压缩时忽略Zip根目录的条目
				if (! keepZipName) {
					//判断是否为根目录(最后一个文件分隔符是唯一的分隔符)
					if (isDir && (FileNameUtil.lastSeparator(entryFileName) == FileNameUtil.lastSeparator(entryFileName.substring(0, entryFileName.length()-1)))) {
						//忽略根目录
						continue;
					}
					//格式化为Linux标准路径名
					entryFileName = FileNameUtil.fileNameFormat(entryFileName, 1);
					//剔除Zip文件的根目录
					entryFileName = entryFileName.substring(entryFileName.indexOf(StringConstant.SEPARATOR)+1);
				}
				//条目的输出路径
				entryFileName = descPath + entryFileName;
				//创建条目文件
				entryFile = new File(entryFileName);
				//如果条目为目录则创建
				if (isDir) {
					FileUtil.addDir(entryFile);
					continue;
				}
				//如果条目为文件则读取并输出，使用NIO（不关闭外部in，关闭本地out）
				NioUtil.copy(zIn,new FileOutputStream(entryFile));
			}
		} catch (IOException e) {
			throw new IoRuntimeException("解压Zip文件失败", e);
		} finally{
			//关闭流资源
			CloseUtil.close(src);
			CloseUtil.close(zIn);
		}
	}
	/**
	 * 压缩Zip文件到本地目录
	 * 
	 * @param src 源文件
	 * @param descPath 目标路径名，须以".zip"结尾
	 * @throws IoRuntimeException 如果src或descPath为空则抛出
	 */
	public static void zip(File src, String descPath){
		//参数校验
		if (src == null || ! src.exists()) {
			throw new ParamRuntimeException("源文件为空或不存在");
		}
		if (StringUtils.isBlank(descPath) || ! descPath.endsWith(ZIP_TAG)) {
			throw new ParamRuntimeException("目标文件名为空或非\".zip\"后缀");
		}
		//目录文件
		File desc = new File(descPath);
		//删除目标文件，防止创建时发生冲突
		FileUtil.delFile(desc);
		//创建目标文件的父目录
		FileUtil.addParentDir(desc);
		//局部变量
		ZipArchiveOutputStream zOut = null;
		ZipArchiveEntry entry = null;
		boolean isDir = src.isDirectory();
		try {
			//创建Zip输出流：根据File
			zOut = new ZipArchiveOutputStream(new File(descPath));
			//添加并输出每个Zip条目（如果是目录则递归，如果是文件则添加文件的Zip条目到Zip输出流）
			outEntry(src, zOut, entry, isDir, StringConstant.EMPTY);
			//完成输出操作
			zOut.finish();
		} catch (IOException e) {
			throw new IoRuntimeException("压缩Zip文件失败："+ e.getMessage(), e);
		} finally {
			//释放流资源
			CloseUtil.close(zOut);
		}
	}
	/**
	 * 压缩Zip文件并输出到OutputStream
	 * 
	 * @param src 源文件
	 * @param desc 
	 * @throws IoRuntimeException 如果src或descPath为空则抛出
	 */
	public static void zip(File src, OutputStream desc){
		//参数校验
		if (src == null || ! src.exists()) {
			throw new ParamRuntimeException("源文件为空或不存在");
		}
		if (desc == null) {
			throw ParamRuntimeException.NULL;
		}
		//局部变量
		ZipArchiveOutputStream zOut = null;
		ZipArchiveEntry entry = null;
		boolean isDir = src.isDirectory();
		try {
			//创建Zip输出流：根据OutputStream
			zOut = new ZipArchiveOutputStream(desc);
			//添加并输出每个Zip条目（如果是目录则递归，如果是文件则添加文件的Zip条目到Zip输出流）
			outEntry(src, zOut, entry, isDir, StringConstant.EMPTY);
			//完成输出操作
			zOut.finish();
		} catch (IOException e) {
			throw new IoRuntimeException(EXP_ZIP_OUT + e.getMessage(), e);
		} finally {
			//释放流资源
			CloseUtil.close(zOut);
		}
	}
	/**
	 * 压缩指定的多个文件为Zip并输出到OutputStream
	 * 
	 * @param srcs 源文件名的集合
	 * @param desc 
	 * @throws IoRuntimeException 如果src或descPath为空则抛出
	 */
	public static void zip(List<String> srcs, OutputStream desc){
		//参数校验
		if (CollectionUtils.isEmpty(srcs) || desc == null) {
			throw ParamRuntimeException.NULL;
		}
		//获取文件集
		List<File> files = new ArrayList<>();
		File file;
		for (String src : srcs) {
			file = new File(src);
			if (file.exists()) {
				files.add(file);
			}else{
				throw new ParamRuntimeException("文件不存在");
			}
		}
		//局部变量
		ZipArchiveOutputStream zOut = null;
		ZipArchiveEntry entry = null;
		boolean isDir;
		try {
			//创建Zip输出流：根据OutputStream
			zOut = new ZipArchiveOutputStream(desc);
			//添加并输出每个Zip条目（如果是目录则递归，如果是文件则添加文件的Zip条目到Zip输出流）
			for (File file2 : files) {
				isDir = file2.isDirectory();
				outEntry(file2, zOut, entry, isDir, StringConstant.EMPTY);
			}
			//完成输出操作
			zOut.finish();
		} catch (IOException e) {
			throw new IoRuntimeException(EXP_ZIP_OUT + e.getMessage(), e);
		} finally {
			//释放流资源
			CloseUtil.close(zOut);
		}
	}
	
	/**
	 * 将InsFile压缩为Zip文件并输出到OutputStream
	 * 
	 * @param src 	InsFile(输出流文件，压缩后将关闭每个文件的输入流)
	 * @param desc 	输出流，输出后关闭
	 */
	public static void zip(InsFile src, OutputStream desc){
		//参数校验
		if (src == null || desc == null) {
			throw ParamRuntimeException.NULL;
		}
		//局部变量
		ZipArchiveOutputStream zOut = null;
		ZipArchiveEntry entry = null;
		try {
			//创建Zip输出流：根据OutputStream
			zOut = new ZipArchiveOutputStream(desc);
			//添加并输出每个Zip条目（如果是目录则递归，如果是文件则添加文件的Zip条目到Zip输出流）
			outEntry(src, zOut, entry);
			//完成输出操作
			zOut.finish();
		} catch (IOException e) {
			throw new IoRuntimeException(EXP_ZIP_OUT + e.getMessage(), e);
		} finally {
			//释放流资源
			CloseUtil.close(zOut);
		}
	}
	
	/**
	 * 创建File的Zip条目，添加到Zip输出山流
	 * 
	 * @param src	     源文件
	 * @param zOut    Zip输出流
	 * @param entry	  Zip条目(传递的只是变量的引用，每次添加时都会重新实例化)
	 * @param isDir   是否为目录
	 * @param dirName 目录名称
	 * @throws IOException
	 */
	private static void outEntry(File src,ZipArchiveOutputStream zOut, ZipArchiveEntry entry, boolean isDir, String dirName) throws IOException{
		//如果是目录，递归；如果非目录，添加Zip条目到Zip输出流
		if (isDir) {
			//目录名称
			dirName = dirName + src.getName()+StringConstant.SEPARATOR;
			//递归遍历目录下的所有文件
			File[] files = src.listFiles();
			if(files != null){
				for (File file : files) {
					//递归添加每个文件的条目
					outEntry(file, zOut, entry, file.isDirectory(),dirName);
				}
			}
		}else{
			//创建Zip条目：指定条目的名称
			entry = new ZipArchiveEntry(dirName + src.getName());
			//添加条目到Zip输出流
			zOut.putArchiveEntry(entry);
			//输出数据到条目，使用NIO（关闭本地in，不关闭外地out）
			InputStream in = null;
			try {
				in = new FileInputStream(src);
				IOUtils.copy(in,zOut);
			}finally {
				CloseUtil.close(in);
			}
			//关闭条目操作(添加每个文件条目，需重复四步：创建条目-添加条目到输出流-输出数据到条目-关闭条目)
			zOut.closeArchiveEntry();
		}
	}
 
	/**
	 * 创建InsFile的Zip条目，添加到Zip输出山流
	 * 
	 * @param file	     源文件
	 * @param zOut    Zip输出流
	 * @param entry	  Zip条目(传递的只是变量的引用，每次添加时都会重新实例化)
	 * @throws IOException
	 */
	private static void outEntry(InsFile file,ZipArchiveOutputStream zOut, ZipArchiveEntry entry) throws IOException{
		//如果是目录 
		if (file.isDir()) {
			//获取子文件
			List<InsFile> children = file.getChildren();
			if (children != null) {
				//遍历子文件
				for (InsFile f : children) {
					//递归
					outEntry(f,zOut,entry);
				}
			}
		}else{
			//创建Zip条目：指定条目的名称
			entry = new ZipArchiveEntry(file.getFilePath());
			//添加条目到Zip输出流
			zOut.putArchiveEntry(entry);
			//输出文件数据到条目
			InputStream in = null;
			try {
				in = file.getIns();
				IOUtils.copy(in,zOut);
			}finally {
				CloseUtil.close(in);
			}
			//关闭条目操作(添加每个文件条目，需重复四步：创建条目-添加条目到输出流-输出数据到条目-关闭条目)
			zOut.closeArchiveEntry();
		}
	}
	
	
}
